/*
Kakine
ActionScript Framework
Copyright © 2011, PUNY, LLC.

This file is part of KAKINE.

KAKINE is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

KAKINE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with KAKINE.  If not, see <http://www.gnu.org/licenses/>.
*/ 
package com.puny.mvc
{
	import com.puny.mvc.views.effect.SoundView;
	import com.puny.utils.ObjectFinder;
	
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.errors.IllegalOperationError;
	import flash.events.Event;

	/**
	 * Dispatched when the instance is loaded on the stage
	 * and loads any other child assets successfully.  
	 * @eventType com.puny.mvc.MvcEvent.READY
	 */
	[Event(name="ready", type="com.puny.mvc.MvcEvent")]

	/**
	 * Dispatched when complete method is invoked.
	 * @see com.puny.mvc.BaseObject#completed
	 * @eventType com.puny.mvc.MvcEvent.COMPLETED
	 */
	[Event(name="completed", type="com.puny.mvc.MvcEvent")]
	
	/**
	 * Dispatched when complete method is invoked.
	 * @see com.puny.mvc.BaseObject#completed
	 * @eventType com.puny.mvc.MvcEvent.COMPLETED
	 */
	[Event(name="before_close", type="com.puny.mvc.MvcEvent")]
	
	/**
	 * Dispatched when complete method is invoked.
	 * @eventType com.puny.mvc.MvcEvent.OPENED
	 */
	[Event(name="opened", type="com.puny.mvc.MvcEvent")]
	
	/**
	 * $Revision: 803 $ <br/>
	 * This calss defines fundamental functions for this MVC architecure.
	 * Here is a chart that shows you the method order to be invoked.
	 * <br/>
	 * This is a chart that shows you the method order to be invoked.
	 * Each method can be overrode when you need.
	 * <img src="../../../../images/lifecycle.png"/>
	 * 
	 * @see com.puny.mvc.views.View
	 * @see com.puny.mvc.controllers.Controller
	 */
	public class BaseObject extends Sprite implements IAssetLife
	{
		
		private var _loaded:Boolean = false;
		private var _counter:uint = 0;
		private var _staged:Boolean = false;
		private var _listeners:ListenerManager;
		private var _lifeManager:AssetLifeManager;
		private var _inAddChild:Boolean = false;
		/**
		 * Constructor
		 */
		public function BaseObject(filename:Object=null) {
			super();
			boot(filename);
			if (_lifeManager == null) {
				_lifeManager = new AssetLifeManager(this);
				super.addEventListener(Event.ADDED_TO_STAGE, addedStageHandler, false, 0, true);
				super.addEventListener(Event.REMOVED_FROM_STAGE, removedStageHandler, false, 0, true);
				//				super.addEventListener(Event.REMOVED, removedHandler, false, 0, true);
			}
//			_listeners = new ListenerManager();
//			_lifeManager = new AssetLifeManager(this);
//			super.addEventListener(Event.ADDED_TO_STAGE, addedStageHandler, false, 0, true);
//			super.addEventListener(Event.REMOVED_FROM_STAGE, removedStageHandler, false, 0, true);
//			super.addEventListener(Event.REMOVED, removedHandler, false, 0, true);
		}
		
		/**
		 * 
		 * @return loaded bytes
		 * 
		 */
		public function get bytesLoaded():uint {
			return 0;
		}
		
		/**
		 * 
		 * @return total bytes
		 * 
		 */
		public function get bytesTotal():uint {
			return 0;
		}
		
		public function inAddChild():void {
			_inAddChild = true;
		}
		
		public function outAddChild():void {
			_inAddChild = false;
		}
		
		/**
		 * 
		 * @return this class name
		 * 
		 */
		public function getName():String {
			return ObjectFinder.eliminateObjectName(this.toString());
		}
		
		/**
		 * To start a class.
		 */
		protected function boot(src:Object=null):void {
			throw IllegalOperationError(src + "is not supported by this framework." + src.toString());
		}
		
		/**
		 * loaded handler
		 */
		protected function loaded(event:Event=null):void {
			if (!_loaded) {
				setup();
				_loaded = true;
				initialized(event);
			} else {
				throw new IllegalOperationError("Can't boot more than once.");
			}
		}
		
		private function addedStageHandler(event:Event):void {
			if (!_staged) {
				_staged = true;
				initialized(event);
			}
		}
		
		/**
		 * This will be invoked when all assets are loaded.
		 * @see com.puny.mvc.controllers#boot
		 */ 
		final protected function initialized(event:Event):void {
			if (_counter == 0/*&&_staged&&_loaded*/) {
				ready(event);
			}
		}

		/**
		 * Faster dispatchEvent
		 * @param event
		 */
		override public function dispatchEvent(evt:Event):Boolean {
		 	if (hasEventListener(evt.type) || evt.bubbles) {
		  		return super.dispatchEvent(evt);
		  	}
		 	return true;
		}		

		/**
		 * instantiate soundview with the given url and add it to stage.
		 * @param url
		 * @return instance of SoundView with the given url
		 */
		protected function addSound(url:Object):SoundView {
			var sound:SoundView = new SoundView(url);
			addChild(sound);
			return sound;
		}

		private function doBeforeAdd(child:DisplayObject):void {
			if (_lifeManager == null) {
				_lifeManager = new AssetLifeManager(this);
				super.addEventListener(Event.ADDED_TO_STAGE, addedStageHandler, false, 0, true);
				super.addEventListener(Event.REMOVED_FROM_STAGE, removedStageHandler, false, 0, true);
//				super.addEventListener(Event.REMOVED, removedHandler, false, 0, true);
			}
			if (child is BaseObject) {
				if (!_loaded||stage==null) {
					_counter++;
				}
				child.addEventListener(MvcEvent.READY, readyHandler, false, 0, true);
				child.addEventListener(MvcEvent.COMPLETED, completedHandler, false, 0, true);
				(child as BaseObject).inAddChild();
			}
		}
		
		private function doAfterAdd(child:DisplayObject):void {
			if (child is BaseObject) {
				(child as BaseObject).outAddChild();
			}
		}
		
		private function readyHandler(event:Event):void {
			if (_counter > 0) {
				_counter--;
//				trace(this,":count=",_counter);
				initialized(event);
			}
		}
		
		/**
		 * Extended addChild.
		 * An added chiled with this method will be removed automatically by a controller.
		 * 
		 * @param child any display object 
		 * 
		 * @return The DisplayObject instance that you pass in the child parameter. 
		 */
		override public function addChild(child:DisplayObject):DisplayObject {
			doBeforeAdd(child);
			super.addChild(child);
			doAfterAdd(child);
			return child;
		}
		
		/**
		 * Extended addChild.
		 * An added chiled with this method will be removed automatically by a controller.
		 * 
		 * @param child any display object
		 * @param index The index position to which the child is added.
         * If you specify a currently occupied index position,
         * the child object that exists at that position and
         * all higher positions are moved up one position in the child list. 
		 *
 		 * @return The DisplayObject instance that you pass in the child parameter. 
		 */
		override public function addChildAt(child:DisplayObject, index:int):DisplayObject {
			doBeforeAdd(child);
			super.addChildAt(child, index);
			doAfterAdd(child);
			return child;
		}
		
		/**
		 * Extended removeChild. If this class is not on the stage, this will return null.
		 * The difference from original one is to remove a COMPLETED event listener.
		 *
 		 * @param child any display object
 		 * @return removed display object when it's removed successfully.
		 * Otherwise null if this class is not on the stage.
		 */
		override public function removeChild(child:DisplayObject):DisplayObject {
			child.removeEventListener(MvcEvent.COMPLETED, completedHandler);
			try {
				trace("Removing child:" + child);
				return super.removeChild(child);
			} catch (e:Error) {
				trace(e);
			}
			return null;
		}

		/**
		 * Extended removeChild. If this class is not on the stage, this will return null.
		 *
 		 * @param index The child index of the DisplayObject to remove
 		 * @return Removed display object or null if this class is not on the stage.
		 */
		override public function removeChildAt(index:int):DisplayObject {
			try {
				var child:DisplayObject = getChildAt(index);
				child.removeEventListener(MvcEvent.COMPLETED, completedHandler);
				return super.removeChildAt(index);
			} catch (e:Error) {
				trace(e);
			}
			return null;
		}
		
		override public function addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=false):void {
			if (_listeners == null) {
				_listeners = new ListenerManager();
			}
			if (_lifeManager == null) {
				_lifeManager = new AssetLifeManager(this);
				super.addEventListener(Event.ADDED_TO_STAGE, addedStageHandler, false, 0, true);
				super.addEventListener(Event.REMOVED_FROM_STAGE, removedStageHandler, false, 0, true);
				//				super.addEventListener(Event.REMOVED, removedHandler, false, 0, true);
			}
			_listeners.add(type,listener);
			super.addEventListener(type,listener,useCapture,priority,useWeakReference);
		}
		
		override public function removeEventListener(type:String, listener:Function, useCapture:Boolean=false):void {
			if (_listeners != null) {
				_listeners.remove(type,listener);
			}
			super.removeEventListener(type,listener,useCapture);
		}
		
		/**
		 * <b>Deperecated</b> This is a helper method.
		 * This lets the fifth parameter of addEventListener true
		 * @param type event name
		 * @param fnc listener
		 */
		[Deprecated(replacement="addEventListener")]
		public function addWeakEventListener(type:String, fnc:Function):void {
			addEventListener(type, fnc, false, 0, true);
		}
		
		private function completedHandler(event:Event):void {
			var dobj:DisplayObject = removeChild(event.currentTarget as DisplayObject);
			trace(dobj, "is removed from", this);
		}
		
		/**
		 * To setup controllers or views, override this method.
		 * This is invoked when the givein object in this constructor is ready. If the given object is a file name, this is invoked when the file is loaded.
		 * In this function, this <code>stage</code>, and <code>parent</code> properties are not ready yet. These are avilable only between <code>beforeReady()</code> and <code>afterClose()</code>. 
		 * When views or controllers are added here, a READY event for this instance will be fired after all of those views and controllers are ready.
		 * 
		 */
		protected function setup():void {}

		/**
		 * This method will be invoked before a READY event is dispatched.
		 * From this method and util <code>afterClose()</code>, this <code>stage</code>, and <code>parent</code> properties are available.
		 */
		public function beforeReady():void {}
		
		/**
		 * This method will be invoked after a READY event is dispatched.
		 */
		public function afterReady():void {}

		/**
		 * Override this method to define opening animation.
		 * If this returns TRUE, this opening process will continue.
		 * If this returns FALSE, this opening process will end and afterOpen method will be invoked.
		 * @return TRUE when needs to cotinue. FALSE when needs to finish
		 * @see afterOpen
		 */
		public function procedureOpen():Boolean {return false;}
		/**
		 * Override this method to initialize opening animation
		 */
		public function beforeOpen():void { }
		/**
		 * Override this method to wrapup opening animation
		 * This is not called when this class is completed in procedureOpen.
		 */
		public function afterOpen():void { }
		
		/**
		 * restart MianFrame loop when the loop was stopped.
		 * @return <code>true</code> if the loop starts.
		 */		
		public function restartMainFrame():Boolean {
			if (_lifeManager != null) {
				return _lifeManager.restartMainFrame();
			}
			return false;
		}
		
		/**
		 * Override this method to initialize main procedure
		 * 
		 */
		public function beforeMainFrame():void {}
		
		/**
		 * Override this method to define main procedure.
		 * If this returns TRUE, this main process will continue.
		 * If this returns FALSE, this main process will end and afterMainFrame method will be invoked.
		 * @return TRUE when needs to cotinue. FALSE when needs to finish
		 * @see afterMainFrame
		 */
		public function procedureMainFrame():Boolean {
			return false;
		}
		
		/**
		 * Override this method to wrapup main procedure
		 */
		public function afterMainFrame():void { }

		/**
		 * Override this method to define closing animation.
		 * If this returns TRUE, this closing process will continue.
		 * If this returns FALSE, this closing process will end and afterClose method will be invoked.
		 * @return TRUE when needs to cotinue. FALSE when needs to finish
		 */
		public function procedureClose():Boolean {return false;}
		/**
		 * Override this method to initialize closing animation
		 */
		public function beforeClose():void {}
		/**
		 * Override this method to wrapup closing animation
		 */
		public function afterClose():void {}

		private function ready(event:Event):void {
			if (_staged && _loaded) {
				_lifeManager.ready();
			}
		}
		
		/**
		 * This should be invoked when the class is ready to removed from the stage.
		 */
		final public function complete():void {
			if (_lifeManager != null) {
				_lifeManager.complete();
			}
		}
		
		/**
		 * This method will be invoked before all children are removed.
		 */
		protected function beforeChildrenRemoved():void {}
		
		/**
		 * This method will be invoked after all children are removed.
		 */
		protected function afterChildrenRemoved():void {}
		
		/**
		 * Remove all entries from the given array object.
		 * @param arr Array instance
		 * 
		 */
		protected function emptyArray(arr:Array):void {
			var obj:DisplayObject;
			while(arr.length > 0) {
				obj = arr.pop();
				if (this.contains(obj)) {
					removeChild(obj);
				}
			}
		}
		
		final private function removedStageHandler(event:Event):void {
			
			beforeChildrenRemoved();
//			removeEventListener(Event.REMOVED_FROM_STAGE, removedStageHandler);
			var index:int = 0;
			while(this.numChildren > index) {
				var obj:Object = this.getChildAt(index);
				if (obj is BaseObject) {
					trace(obj," complete in ", this);
					obj.complete();
					index++;
				} else {
					trace(obj," removed in ", this);
					removeChildAt(index);
				}
			}
			_staged = false;
			afterChildrenRemoved();
//			_lifeManager = null;
			_listeners.cleanUpListeners(this);
			_lifeManager.cleanUp();
			super.removeEventListener(Event.ADDED_TO_STAGE, addedStageHandler);
			super.removeEventListener(Event.REMOVED_FROM_STAGE, removedStageHandler);
			//			super.addEventListener(Event.REMOVED, removedHandler, false, 0, true);
			_listeners = null;
			_lifeManager = null;
		}
		
//		final private function removedHandler(event:Event):void {
//			if (event.target == this&&!_inAddChild) {
//				_listeners.cleanUpListeners(this);
//			}
//		}
		
		
		/**
		 * Find an instance of the given class on the stage.
		 * @param cls class name
		 * @return instance of the give class. <code>null</code> if no instance found.
		 * 
		 */
		public function findInstance(cls:Class):Object {
			var index:int;
			var obj:Object;
			for (index = 0;index < numChildren;index++) {
				obj = getChildAt(index);
				if (obj is cls) {
					return obj as Object;
				}
				if (obj is BaseObject) {
					obj = obj.findInstance(cls);
					if (obj != null) {
						return obj as Object;	
					}
				}
			}
			return null;
		}
		
		/**
		 * get the root class as a BaseObject. This is null when this class instace is not on stage.
		 */
		public function get mainApp():BaseObject {
			return root as BaseObject;
		}
		
		/**
		 * pause open procedure or main procedure when it's running.
		 * 
		 */
		public function pause():void {
			if (_lifeManager != null) {
				_lifeManager.pause();
			}
		}		

		/**
		 * resume open procedure or main procedure when it stops.
		 * 
		 */
		public function resume():void {
			if (_lifeManager != null) {
				_lifeManager.resume();
			}
		}
		
		/**
		 * @return url base as a string
		 * @throws TypeError when this class is not on the stage.
		 */
		protected function get urlBase():String {
			return root.loaderInfo.url.substr(0, root.loaderInfo.url.length - root.toString().length+5);
		}
		
		/**
		 * 
		 * @return <code>true</code> if it's running on a local.
		 * @throws TypeError when this class is not on the stage.
		 */
		protected function isDebug():Boolean {
			return urlBase.substr(0,4) == "file";
		}
	}
}
